﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;
using System.Security.Cryptography;

namespace NetworkLibrary
{
    public class Server
    {
        #region Variables
        public Socket serverSocket;
        List<NetworkLibrary.NetworkClient> clients;
        RSAParameters publicKey, privateKey;
        int port = 9050;
        byte[] command = new byte[1024]; //Contains the last command sent
        byte[] dataToBeSent;
        int dataToBeSentType;
        string dir = "C:\\";
        #endregion

        #region Events
        private event Common.OnClientConnected onClientConnected;
        private event Common.OnClientDisConnected onClientDisconnected;
        private event Common.OnShuttingDown onShutDown;
        private event Common.DataReceived onDataReceived;
        private event Common.Log onLog;
        private event Common.OnFileReceived onFileReceived;
        private event Common.ProgressUpdate fileProgressUpdates;
        private event Common.OnFileTransferStart onFileTransferStart;
#endregion

        /// <summary>
        /// Starts a network server to send data between server and clients
        /// Designed for the Friendly Fire Application
        /// </summary>
        /// <param name="publicKey">An RSA Public Key. Sent to clients that connect.</param>
        /// <param name="privateKey">An RSA Private Key. Used to decode messages that are received.</param>
        public Server(RSAParameters publicKey, RSAParameters privateKey)
        {
            this.publicKey = publicKey;
            this.privateKey = privateKey;
            clients = new List<NetworkClient>();
            StartServerSocket();
        }

        /// <summary>
        /// Starts a network server to send data between server and clients
        /// Designed for the Friendly Fire Application
        /// </summary>
        /// <param name="publicKey">An RSA Public Key. Sent to clients that connect.</param>
        /// <param name="privateKey">An RSA Private Key. Used to decode messages that are received.</param>
        /// <param name="port">Allows the server to be started on a port other than the default (9050)</param>
        public Server(RSAParameters publicKey, RSAParameters privateKey, int port)
        {
            this.publicKey = publicKey;
            this.privateKey = privateKey;
            this.port = port;
            clients = new List<NetworkClient>();
            StartServerSocket();
        }

        /// <summary>
        /// Starts the servers socket to accept connections.
        /// </summary>
        void StartServerSocket()
        {
            serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            
            //Binds the server to listen on port 9050
            IPEndPoint iep = new IPEndPoint(IPAddress.Any, port);
            serverSocket.Bind(iep);
            serverSocket.Listen(5);

            serverSocket.LingerState = new LingerOption(true, 20);
            serverSocket.Ttl = 3;
            serverSocket.DontFragment = false;

            //Start accepting connections
            serverSocket.BeginAccept(new AsyncCallback(AcceptConn), serverSocket);
        }

        #region Asynchronous Calls
        /// <summary>
        /// Allows the Server to accept incoming connection requests
        /// </summary>
        /// <param name="iar"></param>
        void AcceptConn(IAsyncResult iar)
        {
            Socket client = (Socket)iar.AsyncState;

            client = client.EndAccept(iar);

            //Receive the length of the client name (Nickname)
            int clientNameLength = Common.ReceiveInt(client);
            //Receive the clients name (Nickname)
            string clientName = Common.ReceiveString(client, clientNameLength);

            //Get Public Key of the connecting client
            RSACryptoServiceProvider provider = new RSACryptoServiceProvider();
            provider.FromXmlString(Common.ReceiveString(client, Common.ReceiveInt(client)));
            RSAParameters t = provider.ExportParameters(false);

            //Send Servers Public Key
            RSACryptoServiceProvider provider2 = new RSACryptoServiceProvider();
            provider2.ImportParameters(publicKey);
            string xml = provider2.ToXmlString(false);
            Common.Send(client, xml.Length);
            Common.Send(client, xml);

            //Creates a NetworkClient which stores all data for the server-client connection
            NetworkClient networkClient = new NetworkClient(client, clientName, t);
            clients.Add(networkClient);
            if (onClientConnected != null)
                onClientConnected(networkClient);

            client.BeginReceive(command, 0, Common.BufferLength, SocketFlags.None, new AsyncCallback(Command), client);
            serverSocket.BeginAccept(new AsyncCallback(AcceptConn), serverSocket);
        }
        void Command(IAsyncResult iar)
        {
            Socket client = (Socket)iar.AsyncState;
            if (client.Connected)
            {
                try
                {
                    int recv = client.EndReceive(iar);
                    string receivedData = Encoding.ASCII.GetString(command, 0, recv);
                    if (client.Connected)
                    {
                        if (receivedData == "ReceiveData")
                        {
                            int pos = FindClient(client);
                            if (pos > -1)
                            {
                                NetworkData tempData = Common.ReceiveNonChunkedData(clients[pos]);
                                if (onDataReceived != null)
                                    onDataReceived(tempData);
                            }
                            client.BeginReceive(command, 0, Common.BufferLength, SocketFlags.None, new AsyncCallback(Command), client);
                        }
                        else
                            if (receivedData == "ReceiveLargeData")
                            {
                                int pos = FindClient(client);
                                if (pos > -1)
                                {
                                    NetworkData tempData = Common.ReceiveData(clients[pos]);
                                    if (onDataReceived != null)
                                        onDataReceived(tempData);
                                }
                                client.BeginReceive(command, 0, Common.BufferLength, SocketFlags.None, new AsyncCallback(Command), client);
                            }
                            else
                                if (receivedData == "Shutdown")
                                {
                                    int pos = FindClient(client);
                                    if (pos > -1)
                                    {
                                        clients[pos].ClientSocket.Shutdown(SocketShutdown.Both);
                                        clients[pos].ClientSocket.Disconnect(false);
                                        if (onClientDisconnected != null)
                                            onClientDisconnected(clients[pos]);
                                        clients.RemoveAt(pos);
                                    }
                                }
                                else
                                    if (receivedData == "ReceiveFile")
                                    {
                                        int pos = FindClient(client);
                                        if (pos > -1)
                                        {
                                            FileReceivedData file = Common.ReceiveFile(clients[pos], dir, fileProgressUpdates, onFileTransferStart);
                                            if (onFileReceived != null)
                                                onFileReceived(file);
                                        }
                                        client.BeginReceive(command, 0, Common.BufferLength, SocketFlags.None, new AsyncCallback(Command), client);
                                    }
                                    else
                                        if (receivedData == "PublicKey")
                                        {
                                            RSACryptoServiceProvider provider2 = new RSACryptoServiceProvider();
                                            provider2.ImportParameters(publicKey);
                                            string xml = provider2.ToXmlString(false);
                                            Common.Send(client, xml.Length);
                                            Common.Send(client, xml);

                                            RSACryptoServiceProvider provider = new RSACryptoServiceProvider();
                                            provider.FromXmlString(Common.ReceiveString(client, Common.ReceiveInt(client)));
                                            RSAParameters t = provider.ExportParameters(false);

                                            int pos = FindClient(client);
                                            if (pos > -1)
                                            {
                                                clients[pos].SetEncyrption(new EncryptedTransfer(privateKey, t));
                                            }
                                            client.BeginReceive(command, 0, Common.BufferLength, SocketFlags.None, new AsyncCallback(Command), client);
                                        }
                    }
                }
                catch (Exception err)
                {
                }
            }
        }
        void SendDataAsync(IAsyncResult iar)
        {
            Socket client = (Socket)iar.AsyncState;
            if (client.Connected)
            {
                client.EndSend(iar);

                int pos = FindClient(client);
                if (pos > -1)
                {
                    Common.SendNonChunkedData(dataToBeSent, dataToBeSentType, clients[pos]);
                }
            }
            //I don't think this is needed
            //client.BeginReceive(command, 0, Common.BufferLength, SocketFlags.None, new AsyncCallback(Command), client);
        }
        void SendLargeDataAsync(IAsyncResult iar)
        {
            Socket client = (Socket)iar.AsyncState;
            if (client.Connected)
            {
                client.EndSend(iar);

                int pos = FindClient(client);
                if (pos > -1)
                {
                    //Common.SendNonChunkedData(dataToBeSent, dataToBeSentType, clients[pos]);
                    Common.SendData(dataToBeSent, dataToBeSentType, clients[pos]);
                }
            }
            //I don't think this is needed
            //client.BeginReceive(command, 0, Common.BufferLength, SocketFlags.None, new AsyncCallback(Command), client);
        }
        void SendFileAsync(IAsyncResult iar)
        {
            Socket client = (Socket)iar.AsyncState;
            if (client.Connected)
            {
                client.EndSend(iar);

                int pos = FindClient(client);
                if (pos > -1)
                {
                    //Common.SendNonChunkedData(dataToBeSent, dataToBeSentType, clients[pos]);
                    //Common.SendData(dataToBeSent, dataToBeSentType, clients[pos]);
                    Common.SendFile(fileToBeSent, clients[pos], fileProgressUpdates, onFileTransferStart);
                }
            }
            //I don't think this is needed
            //client.BeginReceive(command, 0, Common.BufferLength, SocketFlags.None, new AsyncCallback(Command), client);
        }
        #endregion

        /// <summary>
        /// Finds a client in the clients list based on it's Socket
        /// </summary>
        /// <param name="client">Socket of the client</param>
        /// <returns></returns>
        int FindClient(Socket client)
        {
            for (int i = 0; i < clients.Count; i++)
                if (client == clients[i].ClientSocket)
                    return i;
            return -1;
        }

        /// <summary>
        /// Finds a client based on its Nickname
        /// </summary>
        /// <param name="clientName">String of the client name (Nickname)</param>
        /// <returns></returns>
        Socket FindClient(string clientName)
        {
            for (int i = 0; i < clients.Count; i++)
                if (clientName == clients[i].ClientName)
                    return clients[i].ClientSocket;
            return null;
        }

        #region Commands
        public bool SendData(byte[] data, int dataType, Socket client)
        {
            //Set these temporary values so that the Asynchronous call can access them
            dataToBeSent = data;
            dataToBeSentType = dataType;

            if (client.Connected)
            {
                //Tell the client to Receive Data and then start the Sending Process
                byte[] msg = Encoding.ASCII.GetBytes("ReceiveData");
                client.Send(msg);
                int pos = FindClient(client);
                if (pos > -1)
                {
                    Common.SendNonChunkedData(dataToBeSent, dataToBeSentType, clients[pos]);
                }
                //client.BeginSend(msg, 0, msg.Length, SocketFlags.None, new AsyncCallback(SendDataAsync), client);
                return true;
            }
            return false;
        }
        public bool SendLargeData(byte[] data, int dataType, Socket client)
        {
            //Set these temporary values so that the Asynchronous call can access them
            dataToBeSent = data;
            dataToBeSentType = dataType;

            if (client.Connected)
            {
                //Tell the client to Receive Data and then start the Sending Process
                byte[] msg = Encoding.ASCII.GetBytes("ReceiveLargeData");
                client.BeginSend(msg, 0, msg.Length, SocketFlags.None, new AsyncCallback(SendLargeDataAsync), client);
                return true;
            }
            return false;
        }
        string fileToBeSent = "";
        public bool SendFile(string file, Socket client)
        {
            //Set these temporary values so that the Asynchronous call can access them
            fileToBeSent = file;

            if (client.Connected)
            {
                //Tell the client to Receive Data and then start the Sending Process
                byte[] msg = Encoding.ASCII.GetBytes("ReceiveFile");
                client.Send(msg);
                    int pos = FindClient(client);
                    if (pos > -1)
                    {
                        //Common.SendNonChunkedData(dataToBeSent, dataToBeSentType, clients[pos]);
                        //Common.SendData(dataToBeSent, dataToBeSentType, clients[pos]);
                        Common.SendFile(fileToBeSent, clients[pos], fileProgressUpdates, onFileTransferStart);
                    }

                //client.BeginSend(msg, 0, msg.Length, SocketFlags.None, new AsyncCallback(SendFileAsync), client);
                return true;
            }
            return false;
        }
        public void Shutdown()
        {
            for (int i = 0; i < clients.Count; i++)
            {
                clients[i].ClientSocket.Send(Encoding.ASCII.GetBytes("Shutdown"), SocketFlags.None);
            }
            clients.Clear();
            serverSocket.Disconnect(false);
            if (onShutDown != null)
                onShutDown(serverSocket);
        }
        #endregion

        public string DownloadDirectory { get { return dir; } set { dir = value; } }
        public List<NetworkLibrary.NetworkClient> Clients { get { return clients; } }

        #region Events
        /// <summary>
        /// The Event used when the Client Connects to a server
        /// </summary>
        public event Common.OnClientConnected OnClientConnected
        {
            add { onClientConnected += value; }
            remove { onClientConnected -= value; }
        }
        /// <summary>
        /// The Event used when the Client Disconnects from a server
        /// </summary>
        public event Common.OnClientDisConnected OnClientDisConnected
        {
            add { onClientDisconnected += value; }
            remove { onClientDisconnected -= value; }
        }
        /// <summary>
        /// The Event used when the Client Receives data from a server
        /// </summary>
        public event Common.DataReceived OnDataReceived
        {
            add { onDataReceived += value; }
            remove { onDataReceived -= value; }
        }
        /// <summary>
        /// The Event used when the Client Receives a file from a server
        /// </summary>
        public event Common.OnFileReceived OnFileReceived
        {
            add { onFileReceived += value; }
            remove { onFileReceived -= value; }
        }
        /// <summary>
        /// The Event used when the Client writes something to it's log
        /// Errors, Connections, Disconnects, etc. etc. are logged
        /// </summary>
        public event Common.Log OnLog
        {
            add { onLog += value; }
            remove { onLog -= value; }
        }
        /// <summary>
        /// The Event used when the client shuts down.
        /// </summary>
        public event Common.OnShuttingDown OnShuttindDown
        {
            add { onShutDown += value; }
            remove { onShutDown -= value; }
        }
        /// <summary>
        /// The Event used when the server updates the progress of a file transfer
        /// </summary>
        public event Common.ProgressUpdate FileProgressUpdate
        {
            add { fileProgressUpdates += value; }
            remove { fileProgressUpdates -= value; }
        }
        /// <summary>
        /// The Event used when the server begins a file transfer
        /// </summary>
        public event Common.OnFileTransferStart OnFileTransferStart
        {
            add { onFileTransferStart += value; }
            remove { onFileTransferStart -= value; }
        }
        #endregion
    }
}
